﻿// =====================================================================
//
//  This file is part of the Microsoft Dynamics CRM SDK code samples.
//
//  Copyright (C) Microsoft Corporation.  All rights reserved.
//
//  This source code is intended only as a supplement to Microsoft
//  Development Tools and/or on-line documentation.  See these other
//  materials for detailed information regarding Microsoft code samples.
//
//  THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
//  KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
//  IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
//  PARTICULAR PURPOSE.
//
// =====================================================================
#region Usings

using System;
using System.Diagnostics;
using System.Linq;
using System.ServiceModel.Description;
using Microsoft.Xrm.Sdk.Client.Protocols.WSTrust;
using SL.IdentityModel.Protocols.WSTrust;

#endregion

namespace Microsoft.Xrm.Sdk.Client
{
	internal sealed partial class ServiceConfiguration<TService>
	{
		public event EventHandler<IdentityProvidersLoadCompletedEventArgs> IdentityProviderLoaded;
		public event EventHandler<AuthenticationCompletedEventArgs> AuthenticationComplete;

		public PolicyConfiguration PolicyConfiguration
		{
			get { return this.ServiceEndpointMetadata.PolicyConfiguration; }
		}
		public AuthenticationProviderType AuthenticationType
		{
			get
			{
				if (PolicyConfiguration is WindowsPolicyConfiguration)
				{
					return AuthenticationProviderType.ActiveDirectory;
				}
				if (PolicyConfiguration is ClaimsPolicyConfiguration)
				{
					return AuthenticationProviderType.Federation;
				}
				if (PolicyConfiguration is LiveIdPolicyConfiguration)
				{
					return AuthenticationProviderType.LiveId;
				}
				if (PolicyConfiguration is OnlineFederationPolicyConfiguration)
				{
					return AuthenticationProviderType.OnlineFederation;
				}
				return AuthenticationProviderType.None;
			}
		}
		#region IServiceManagement
		public void Authenticate(AuthenticationCredentials authenticationCredentials)
		{
			ClientExceptionHelper.ThrowIfNull(authenticationCredentials, "authenticationCredentials");

			switch (AuthenticationType)
			{
				case AuthenticationProviderType.OnlineFederation:
					AuthenticateOnlineFederationInternal(authenticationCredentials);
					return;
				case AuthenticationProviderType.Federation:
					AuthenticateFederationInternal(authenticationCredentials);
					return;
				case AuthenticationProviderType.LiveId:
					AuthenticateLiveIdInternal(authenticationCredentials);
					return;
				case AuthenticationProviderType.ActiveDirectory:
					{
						FireAuthenticationCompletedSuccess(authenticationCredentials);
					}
					return;
				default:
					FireAuthenticationCompletedSuccess(authenticationCredentials);
					return;
			}
		}

		private void FireAuthenticationCompletedSuccess(AuthenticationCredentials authenticationCredentials)
		{
			AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(null, false, null, authenticationCredentials);
			FireAuthenticationCompletedEventForExternalCaller(eventArgs);
		}

		private void AuthenticateFederationInternal(AuthenticationCredentials authenticationCredentials)
		{
			if (authenticationCredentials.SecurityTokenResponse != null)
			{
				AuthenticateFederationTokenInternal(authenticationCredentials);
				return;
			}

			if (authenticationCredentials.AppliesTo == null)
			{
				authenticationCredentials.AppliesTo = CurrentServiceEndpoint.Address.Uri;
			}

			authenticationCredentials.EndpointType = GetCredentialsEndpointType(authenticationCredentials.ClientCredentials);

			authenticationCredentials.IssuerEndpoints = authenticationCredentials.HomeRealm != null ? CrossRealmIssuerEndpoints[authenticationCredentials.HomeRealm] : IssuerEndpoints;
			// Return credentials is the same as the authenticating credentials
			AuthenticateAndReturnToExternalCaller(authenticationCredentials, authenticationCredentials);
		}

		/// <summary>
		/// 1. Calls AuthenticateInternal using credentialsForAuthentication
		/// 2. On callback, sets the token response on the returncredentials if success
		/// 3. Fires AuthenticationCompleted event for success/failure cases
		/// </summary>
		/// <param name="credentialsForAuthentication">Credentials used to authenticate</param>
		/// <param name="returnCredentials">Credentials to be returned to the caller</param>
		private void AuthenticateAndReturnToExternalCaller(AuthenticationCredentials credentialsForAuthentication, AuthenticationCredentials returnCredentials)
		{
			AuthenticateInternal(
						 credentialsForAuthentication,
						 (issueCompletedArgs) =>
						 {
							 HandleIssueComplete(returnCredentials, issueCompletedArgs);
						 }
					 );
		}

		private void HandleIssueComplete(AuthenticationCredentials returnCredentials, IssueCompletedEventArgs issueCompletedArgs)
		{
			if (issueCompletedArgs.Error != null || issueCompletedArgs.Result == null)
			{
				AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(issueCompletedArgs.Error, issueCompletedArgs.Cancelled, issueCompletedArgs.UserState, null);
				FireAuthenticationCompletedEventForExternalCaller(eventArgs);
			}
			else
			{
				returnCredentials.SecurityTokenResponse = issueCompletedArgs.Result;

				AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(issueCompletedArgs.Error, issueCompletedArgs.Cancelled, issueCompletedArgs.UserState, returnCredentials);
				FireAuthenticationCompletedEventForExternalCaller(eventArgs);
			}
		}

		private AuthenticationCredentials AuthenticateFederationTokenInternal(AuthenticationCredentials authenticationCredentials)
		{
			AuthenticationCredentials returnCredentials = new AuthenticationCredentials();
			returnCredentials.SupportingCredentials = authenticationCredentials;
			if (authenticationCredentials.AppliesTo == null)
			{
				authenticationCredentials.AppliesTo = CurrentServiceEndpoint.Address.Uri;
			}

			authenticationCredentials.EndpointType = _tokenEndpointType;
			authenticationCredentials.KeyType = String.Empty;
			authenticationCredentials.IssuerEndpoints = IssuerEndpoints;

			AuthenticateAndReturnToExternalCaller(authenticationCredentials, returnCredentials);

			return returnCredentials;
		}
		/// <summary>
		/// Supported matrix:
		/// 1.  AuthenticationCredentials.ClientCredentials MUST be populated with Live ID user name/password.
		/// 2.  Supporting Credentials MUST be populated.
		///		a.  SecurityTokenResponse MUST be populated, or:
		///		b.  Credentials MUST be populated.
		/// </summary>
		/// <param name="authenticationCredentials"></param>
		/// <returns></returns>
		private void AuthenticateLiveIdInternal(AuthenticationCredentials authenticationCredentials)
		{
			ClientExceptionHelper.ThrowIfNull(authenticationCredentials.ClientCredentials, "authenticationCredentials.ClientCredentials");
			ClientExceptionHelper.ThrowIfNullOrEmpty(authenticationCredentials.ClientCredentials.UserName.UserName, "authenticationCredentials.ClientCredentials.UserName.UserName");
			ClientExceptionHelper.ThrowIfNullOrEmpty(authenticationCredentials.ClientCredentials.UserName.Password, "authenticationCredentials.ClientCredentials.UserName.Password");
			ClientExceptionHelper.ThrowIfNull(authenticationCredentials.SupportingCredentials, "authenticationCredentials.SupportingCredentials");

			var onlinePolicy = PolicyConfiguration as OnlinePolicyConfiguration;
			ClientExceptionHelper.ThrowIfNull(onlinePolicy, "onlinePolicy");

			AuthenticationCredentials returnCredentials = new AuthenticationCredentials();

			if (authenticationCredentials.SupportingCredentials.SecurityTokenResponse == null)
			{
				ClientExceptionHelper.ThrowIfNull(authenticationCredentials.SupportingCredentials.ClientCredentials, "authenticationCredentials.SupportingCredentials.ClientCredentials");
				ClientExceptionHelper.ThrowIfNullOrEmpty(authenticationCredentials.SupportingCredentials.ClientCredentials.UserName.UserName, "authenticationCredentials.SupportingCredentials.ClientCredentials.UserName.UserName");
				ClientExceptionHelper.ThrowIfNullOrEmpty(authenticationCredentials.SupportingCredentials.ClientCredentials.UserName.Password, "authenticationCredentials.SupportingCredentials.ClientCredentials.UserName.Password");

				this.liveIdUserCredentials = authenticationCredentials.ClientCredentials;
				this.DeviceSignInComplete += ServiceConfiguration_DeviceSignInComplete;
				AuthenticateDevice(authenticationCredentials.SupportingCredentials.ClientCredentials);
			}
		}

		ClientCredentials liveIdUserCredentials;
		private void ServiceConfiguration_DeviceSignInComplete(object sender, SignOnCompletedEventArgs args)
		{
			this.DeviceSignInComplete -= this.ServiceConfiguration_DeviceSignInComplete;

			if (args.Error != null)
			{
				AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(args.Error, args.Cancelled, args.UserState, null);
				FireAuthenticationCompletedEventForExternalCaller(eventArgs);
			}
			else
			{
				this.SignInComplete += new EventHandler<SignOnCompletedEventArgs>(ServiceConfiguration_SignInComplete);
				this.Authenticate(this.liveIdUserCredentials, args.Result);
			}
			this.liveIdUserCredentials = null;
		}

		private void ServiceConfiguration_SignInComplete(object sender, SignOnCompletedEventArgs args)
		{
			this.SignInComplete -= this.ServiceConfiguration_SignInComplete;
			if (args.Error != null)
			{
				AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(args.Error, args.Cancelled, args.UserState, null);
				FireAuthenticationCompletedEventForExternalCaller(eventArgs);
			}
			else
			{
				var returnCredentials = new AuthenticationCredentials();
				returnCredentials.SecurityTokenResponse = args.Result;

				AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(args.Error, args.Cancelled, args.UserState, returnCredentials);
				FireAuthenticationCompletedEventForExternalCaller(eventArgs);
			}
		}

		/// <summary>
		/// Supported matrix:
		/// 1.  Security Token Response populated: We will submit the token to Org ID to exchange for a CRM token.
		/// 2.  Credentials passed.  
		///		a.  The UserPrincipalName MUST be populated if the Username/Windows username is empty AND the Home Realm Uri is null.
		///		a.  If the Home Realm 
		/// </summary>
		/// <param name="authenticationCredentials"></param>
		/// <returns></returns>
		private void AuthenticateOnlineFederationInternal(AuthenticationCredentials authenticationCredentials)
		{
			var onlinePolicy = PolicyConfiguration as OnlinePolicyConfiguration;
			ClientExceptionHelper.ThrowIfNull(onlinePolicy, "onlinePolicy");

			OrgIdentityProviderTrustConfiguration liveTrustConfig = onlinePolicy.OnlineProviders.Values.OfType<OrgIdentityProviderTrustConfiguration>().FirstOrDefault();
			ClientExceptionHelper.ThrowIfNull(liveTrustConfig, "liveTrustConfig");

			//Two scenarios:
			//1.  Managed Credentials
			//2.  Federated Credentials
			//3.  A token to submit to OrgID.
			if (authenticationCredentials.SecurityTokenResponse != null)
			{
				AuthenticateOnlineFederationTokenInternal(liveTrustConfig, authenticationCredentials);
				return;
			}

			Boolean authWithOrgId = true;

			if (authenticationCredentials.HomeRealm == null)
			{
				string userPrincipalName;
				if (String.IsNullOrEmpty(authenticationCredentials.UserPrincipalName))
				{
					userPrincipalName = GetUserPrincipalName(authenticationCredentials.ClientCredentials);
				}
				else
				{
					userPrincipalName = authenticationCredentials.UserPrincipalName;
				}

				GetIdentityProviderInternal(userPrincipalName,
					(source, args) =>
					{
						var identityProvider = args.Result;
						ClientExceptionHelper.ThrowIfNull(identityProvider, "identityProvider");
						authenticationCredentials.HomeRealm = identityProvider.ServiceUrl;
						authWithOrgId = (identityProvider.IdentityProviderType == IdentityProviderType.OrgId);
						if (authWithOrgId)
						{
							ClientExceptionHelper.Assert(onlinePolicy.OnlineProviders.ContainsKey(authenticationCredentials.HomeRealm), "Online Identity Provider NOT found!  {0}", identityProvider.ServiceUrl);
						}

						if (authWithOrgId)
						{
							//We are authenticating against OrgID with the credentials;
							authenticationCredentials.AppliesTo = new Uri(liveTrustConfig.AppliesTo);
							authenticationCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[authenticationCredentials.HomeRealm];
							authenticationCredentials.EndpointType = TokenServiceCredentialType.Username;
							AuthenticateAndReturnToExternalCaller(authenticationCredentials, authenticationCredentials);
							return;
						}

						//We are authenticating against ADFS with the credentials.
						authenticationCredentials.AppliesTo = liveTrustConfig.Identifier;
						authenticationCredentials.KeyType = WSTrust13Constants.KeyTypes.Bearer;
						authenticationCredentials.EndpointType = GetCredentialsEndpointType(authenticationCredentials.ClientCredentials);


						this.AuthenticateCrossRealmCore(authenticationCredentials.HomeRealm,
							() =>
							{
								authenticationCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[authenticationCredentials.HomeRealm];

								AuthenticateInternal(authenticationCredentials,
									(issueCompletedArgs) =>
									{
										if (issueCompletedArgs.Error != null || issueCompletedArgs.Result == null)
										{
											AuthenticationCompletedEventArgs eventArgs = new AuthenticationCompletedEventArgs(issueCompletedArgs.Error, issueCompletedArgs.Cancelled, issueCompletedArgs.UserState, null);
											FireAuthenticationCompletedEventForExternalCaller(eventArgs);
										}
										else
										{
											authenticationCredentials.SecurityTokenResponse = issueCompletedArgs.Result;

											AuthenticationCredentials returnCredentials = new AuthenticationCredentials();
											returnCredentials.SupportingCredentials = authenticationCredentials;
											returnCredentials.AppliesTo = new Uri(liveTrustConfig.AppliesTo);
											returnCredentials.IssuerEndpoints = CrossRealmIssuerEndpoints[liveTrustConfig.Endpoint.GetServiceRoot()];
											returnCredentials.EndpointType = TokenServiceCredentialType.SymmetricToken;
											returnCredentials.RequestType = WSTrustConstants.WSTrust2005.RequestTypes.Issue;
											AuthenticateAndReturnToExternalCaller(returnCredentials, returnCredentials);
										}
									}
								);
							});
					}
				);
			}
		}

		private void AuthenticateOnlineFederationTokenInternal(IdentityProviderTrustConfiguration liveTrustConfig, AuthenticationCredentials authenticationCredentials)
		{
			AuthenticationCredentials returnCredentials = new AuthenticationCredentials();
			returnCredentials.SupportingCredentials = authenticationCredentials;

			String appliesTo = authenticationCredentials.AppliesTo != null ? authenticationCredentials.AppliesTo.AbsoluteUri : liveTrustConfig.AppliesTo;
			Uri tokenEndpoint = authenticationCredentials.HomeRealm ?? liveTrustConfig.Endpoint.GetServiceRoot();

			AuthenticateCrossRealmInternal(authenticationCredentials.SecurityTokenResponse, appliesTo, tokenEndpoint,
				(issueCompletedArgs) =>
				{
					HandleIssueComplete(returnCredentials, issueCompletedArgs);
				}
			);
		}

		#endregion IServiceManagement

		internal string GetUserPrincipalName(ClientCredentials clientCredentials)
		{
			String userName = String.Empty;

			if (!String.IsNullOrWhiteSpace(clientCredentials.UserName.UserName))
			{
				userName = ExtractUserName(clientCredentials.UserName.UserName);
			}
			ClientExceptionHelper.Assert(!String.IsNullOrEmpty(userName), "clientCredentials.UserName.UserName or clientCredentials.Windows.ClientCredential.UserName MUST be populated!");
			return userName;
		}

		public void GetIdentityProvider(String userPrincipalName)
		{
			GetIdentityProviderInternal(userPrincipalName);
		}

		private EventHandler<IdentityProvidersLoadCompletedEventArgs> identityProvidersHandler;

		private void GetIdentityProviderInternal(String userPrincipalName, EventHandler<IdentityProvidersLoadCompletedEventArgs> callback = null)
		{
			Debug.Assert(identityProvidersHandler == null);

			if (callback == null)
			{
				identityProvidersHandler = (s, args) => IdentityProviderLoaded(this, args);
			}
			else
			{
				identityProvidersHandler = callback;
			}

			Debug.Assert(identityProvidersHandler != null);

			IdentityProviderTrustConfiguration idp = TryGetOnlineTrustConfiguration();
			if (idp == null)
			{
				identityProvidersHandler(this, null);
				return;
			}
			IdentityProviderLookup.Instance.identityProvidersLoaded += new EventHandler<IdentityProvidersLoadCompletedEventArgs>(
				(s, e) =>
				{
					if (e.UserPrincipalName.Equals(userPrincipalName))
					{
						IdentityProviderLookup.Instance.identityProvidersLoaded -= identityProvidersHandler;
						identityProvidersHandler(this, e);
					}
				});

			IdentityProviderLookup.Instance.GetIdentityProviderAsync(idp.Endpoint.GetServiceRoot(), idp.Endpoint.GetServiceRoot(), userPrincipalName);
		}

		private IdentityProviderTrustConfiguration TryGetOnlineTrustConfiguration()
		{
			var liveConfiguration = PolicyConfiguration as OnlinePolicyConfiguration;
			if (liveConfiguration == null)
			{
				return null;
			}

			return liveConfiguration.OnlineProviders.Values.OfType<OrgIdentityProviderTrustConfiguration>().FirstOrDefault();
		}

		private String ExtractUserName(String userName)
		{
            return userName.Contains("@") ? userName : String.Empty;
        }

		private void FireAuthenticationCompletedEventForExternalCaller(AuthenticationCompletedEventArgs args)
		{
			if (null != AuthenticationComplete)
			{
				AuthenticationComplete(this, args);
			}
		}
	}
}
